Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Net

Namespace Annotations

  ''' <summary>
  ''' Interface meaning that the item can be selected and that this action will generate an event.
  ''' </summary>
  ''' <remarks></remarks>
  Public Interface ISelectable
    Event SelectedChanged(ByVal sender As ISelectable)
    Property Selected() As Boolean
  End Interface

  ''' <summary>
  ''' Interface meaning that the item can be locked.
  ''' Use this to prevent modifications from the UI, no internal checks are actually performed so programmatic changes are not impaired.
  ''' </summary>
  ''' <remarks>Easiest if implemented in parent of Annotations class which does UI.</remarks>
  Public Interface ILockable
    Property Locked() As Boolean
  End Interface

  ''' <summary>
  ''' Top level class for annotations
  ''' </summary>
  ''' <remarks></remarks>
  Public Class Annotations
    Inherits CollectionWithEventsTemplate(Of AnnotationContainer)
    Implements IDisposable

    Public Event AnnotationContainerChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
    Private Sub OnAnnotationContainerChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
      If Me.Events Then RaiseEvent AnnotationContainerChanged(sender, e)
    End Sub

    Public Event AnnotationChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
    Private Sub OnAnnotationChanged(ByVal sender As Object, ByVal e As CollectionChangedEventArgs)
      If Me.Events Then RaiseEvent AnnotationChanged(sender, e)
    End Sub

#Region "Constructor - destructor"
    ''' <summary>
    ''' Create a new Annotations object.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
      MyBase.New()
    End Sub

    Public Overridable Overloads Sub Dispose() Implements IDisposable.Dispose
      Dispose(True)
      GC.SuppressFinalize(Me)
    End Sub

    Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
      'We clean up, the program leaks memory otherwise due to unmanaged GDI+ objects
      Debug.WriteLine("AnnotationPoints.Dispose")
      If disposing Then
        ' Free other state (managed objects).
        MyBase.Clear()
      End If
    End Sub
#End Region

#Region "Properties"
    ''' <summary>
    ''' Obtain or set item with the given key. If the key does not exist a new item is created when setting.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Property AnnotationContainer(ByVal key As String) As AnnotationContainer
      Get
        Dim container As AnnotationContainer = Nothing
        MyBase.TryGetValue(key, container)
        Return container
      End Get
      Set(ByVal value As AnnotationContainer)
        InitializeAnnotationContainer(key, value)
        MyBase.Item(key) = value
      End Set
    End Property

    Public Overloads ReadOnly Property AnnotationContainer(ByVal index As Integer) As AnnotationContainer
      Get
        Return Me.Item(index)
      End Get
    End Property

    Private _dimensionMode As LineAndTextDimensionMode
    ''' <summary>
    ''' How to interpret the line and text dimensions: in image or screen coordinates.
    ''' When in image coordinates it scales with the underlying image when zooming, otherwise it does not.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property DimensionMode() As LineAndTextDimensionMode
      Get
        Return Me._dimensionMode
      End Get
      Set(ByVal value As LineAndTextDimensionMode)
        Me._dimensionMode = value
      End Set
    End Property

    Private _drawingInfo As New DrawingInfo
    ''' <summary>
    ''' The drawing info can be used to obtain drawing pens and line and text dimensions.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property DrawingInfo() As DrawingInfo
      Get
        Return Me._drawingInfo
      End Get
    End Property
#End Region

#Region "Methods"

    Private Sub InitializeAnnotationContainer(ByVal key As String, ByVal value As AnnotationContainer)
      AddHandler value.Changed, AddressOf OnAnnotationContainerChanged
      AddHandler value.AnnotationChanged, AddressOf OnAnnotationChanged
      AddHandler value.SelectedChanged, AddressOf OnSelectedChangedEvent
      value._key = key
      value._parent = Me
    End Sub

    ''' <summary>
    ''' Add a annotation container.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <param name="value"></param>
    ''' <remarks></remarks>
    Public Overrides Sub Add(ByVal key As String, ByVal value As AnnotationContainer)
      'This propagates the point collection changed event up the hierarchy.
      InitializeAnnotationContainer(key, value)
      MyBase.Add(key, value)
    End Sub

#If IMAGEPROCESSING Then
    ''' <summary>
    ''' Add one or more annotation containers by extracting them from an image. Supports 1 and 8 bit / pixel images, 
    ''' and for 8-bit images each pixel value will give rise to an annotation container if the image has pixels with this value.
    ''' </summary>
    ''' <param name="key"></param>
    ''' <param name="bitmap"></param>
    ''' <param name="scale" ></param>
    ''' <remarks>Mask images are often undersampled, so we must pass the relative scale of the annotation versus the mask. By definition the entry with 
    ''' the transparent color is not used as it indicates background</remarks>
    Public Overloads Sub Add(ByVal key As String, ByVal bitmap As Bitmap, ByVal scale As Single)
      Select Case bitmap.PixelFormat
        Case Imaging.PixelFormat.Format8bppIndexed
          Dim palette As Drawing.Imaging.ColorPalette = bitmap.Palette
          For i As Byte = 0 To 255
            If palette.Entries(i).A <> 0 Then
              Dim mask1 As Bitmap = ImageProcessing.Image8.Extract(bitmap, i)
              If ImageProcessing.Image1.CountPixels(mask1) > 1 Then
                Me.Add(key & i, CIPF.Annotations.AnnotationContainer.FromImage(mask1, scale))
              End If
            End If
          Next
        Case Imaging.PixelFormat.Format1bppIndexed
          Me.Add(key, CIPF.Annotations.AnnotationContainer.FromImage(bitmap, scale))
      End Select
    End Sub
#End If

    ''' <summary>
    ''' Scale all polygons inside container.
    ''' </summary>
    ''' <param name="sScale"></param>
    ''' <remarks>
    ''' </remarks>
    Public Overridable Sub Scale(ByVal sScale As Single)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Scale(sScale)
      Next
    End Sub

    ''' <summary>
    ''' Scale all polygons inside all containers.
    ''' </summary>
    ''' <param name="scale"></param>
    ''' <param name="refPt"></param>
    ''' <remarks></remarks>
    Public Sub Scale(ByVal scale As Double, ByVal refPt As PointF)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Scale(scale, refPt)
      Next
    End Sub

    ''' <summary>
    ''' Move all annotations inside  all containers.
    ''' </summary>
    ''' <param name="dx"></param>
    ''' <param name="dy"></param>
    Public Sub Move(ByVal dx As Single, ByVal dy As Single)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Move(dx, dy)
      Next
    End Sub

    ''' <summary>
    ''' Quantize all annotations inside  all containers.
    ''' </summary>
    ''' <param name="sMinDist"></param>
    ''' <param name="sMaxDist"></param>
    ''' <remarks></remarks>
    Public Sub Quantize(ByVal sMinDist As Double, ByVal sMaxDist As Double)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Quantize(sMinDist, sMaxDist)
      Next
    End Sub

    ''' <summary>
    ''' Expand all annotations inside all containers.
    ''' </summary>
    ''' <param name="distance"></param>
    ''' <remarks></remarks>
    Public Overloads Sub Expand(ByVal distance As Single)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Expand(distance)
      Next
    End Sub

    ''' <summary>
    ''' Rotate all annotations inside all containers.
    ''' </summary>
    ''' <param name="angle"></param>
    ''' <param name="refPt"></param>
    ''' <remarks></remarks>
    Public Sub Rotate(ByVal angle As Double, ByVal refPt As PointF)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Rotate(angle, refPt)
      Next
    End Sub

    ''' <summary>
    ''' Flip all annotations inside all containers.
    ''' </summary>
    ''' <param name="x"></param>
    ''' <remarks></remarks>
    Public Sub Flip(ByVal x As Double, ByVal axis As Integer)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Flip(x, axis)
      Next
    End Sub

    ''' <summary>
    ''' Transform all annotations inside this polySet
    ''' </summary>
    ''' <param name="m"></param>
    ''' <remarks></remarks>
    Public Sub Transform(ByVal m As Matrix)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Transform(m)
      Next
    End Sub

    ''' <summary>
    ''' Clip all annotations inside all containers.
    ''' </summary>
    ''' <param name="rect"></param>
    ''' <remarks></remarks>
    Public Sub Clip(ByVal rect As RectangleF)
      For Each polySet As AnnotationContainer In Me.Values
        polySet.Clip(rect)
      Next
    End Sub
#End Region

#Region "IO Methods"
    ''' <summary>
    ''' Load a set of annotation containers from a SVG file in an URI.
    ''' </summary>
    ''' <param name="oUri"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Function Load(ByVal oUri As Uri) As Boolean
      Return Load(oUri, "")
    End Function

    ''' <summary>
    ''' Load a set of annotation containers from a SVG file in an URI, from a given session on a web server.
    ''' </summary>
    ''' <param name="oUri"></param>
    ''' <param name="sessionID"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overloads Function Load(ByVal oUri As Uri, ByVal sessionID As String) As Boolean
      Dim SVGDoc As New Xml.XmlDocument

      'ALWAYS Clears the collection, even if failure!!!!
      MyBase.Clear()

      If oUri.IsFile Then
        'It is very important to set the resolvers to nothing, else the 
        'XML files will be checked vs their schemas, thereby connecting to the internet
        'and needing proxy authentication if we are behind a proxy!

        'COMMENT. Try .. catch is quite slow, and as the annotations file will often be missing, we
        'test for this first
        If System.IO.File.Exists(oUri.LocalPath) Then
          Try
            Dim txtReader As New System.IO.StreamReader(oUri.LocalPath)
            Dim txtXMLReader As New Xml.XmlTextReader(txtReader)
            txtXMLReader.XmlResolver = Nothing
            SVGDoc.XmlResolver = Nothing
            SVGDoc.Load(txtXMLReader)
            txtReader.Close()
          Catch ex As Exception
            Debug.WriteLine("Failed to read file URI " & oUri.ToString & " (" & ex.Message & ")", "AnnotationContainers.Load")
            Return False
          End Try
        Else
          Debug.WriteLine("File URI " & oUri.ToString & " does not exist", "AnnotationContainers.Load")
          Return False
        End If
      Else
        Try
          Dim oStream As System.IO.Stream = IOUtil.GetHTTPDownloadStream(oUri, sessionID)
          SVGDoc.XmlResolver = Nothing
          SVGDoc.Load(oStream)
          oStream.Close()
        Catch ex As Exception
          Debug.WriteLine("Failed to read URI stream " & oUri.ToString & " (" & ex.Message & ")", "AnnotatedImage.LoadAnnotations")
          Return False
        End Try
      End If

      'Now look for g, they represent annotation sets
      Dim oCNodes As Xml.XmlNodeList = SVGDoc.SelectNodes("/svg/g")
      Dim oCNode As Xml.XmlNode
      For Each oCNode In oCNodes
        Dim annContainer As AnnotationContainer = CIPF.Annotations.AnnotationContainer.FromXML(oCNode)
        Dim strCKey As String = oCNode.Attributes.GetNamedItem("id").Value
        InitializeAnnotationContainer(strCKey, annContainer)
        MyBase.Add(strCKey, annContainer)
      Next
      Return True
    End Function

    ''' <summary>
    ''' Save the annotation containers to a SVG file.
    ''' </summary>
    ''' <param name="oUri"></param>
    ''' <remarks></remarks>
    Public Overloads Sub Save(ByVal oUri As Uri)
      Save(oUri, "")
    End Sub

    ''' <summary>
    ''' Save the annotation containers to a SVG file on a webserver and in a certain session.
    ''' </summary>
    ''' <param name="oUri"></param>
    ''' <param name="sessionID"></param>
    ''' <remarks></remarks>
    Public Overloads Sub Save(ByVal oUri As Uri, ByVal sessionID As String)
      'Save annotations as SVG in URI. This allows annotations to be viewed 
      'in a browser , e.g. with the adobe plugin.
      Debug.WriteLine("Saving/Streaming annotations to " & oUri.ToString, "Annotations.Save")
      Dim numberFormat As System.Globalization.NumberFormatInfo = System.Globalization.NumberFormatInfo.InvariantInfo

      If Me.Count > 0 Then
        Dim SVGXMLFile As Xml.XmlTextWriter
        Dim request As HttpWebRequest = Nothing

        If oUri.IsFile Then
          SVGXMLFile = New Xml.XmlTextWriter(oUri.LocalPath, System.Text.Encoding.UTF8)
        Else
          request = IOUtil.GetHTTPUploadRequest(oUri, sessionID)
          SVGXMLFile = New Xml.XmlTextWriter(request.GetRequestStream, System.Text.Encoding.UTF8)
        End If

        SVGXMLFile.WriteStartDocument(False)
        SVGXMLFile.WriteDocType("svg", Nothing, "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", Nothing)
        SVGXMLFile.WriteStartElement("svg")

        'Each annotationcontainer will be in its SVG group (g element)
        For Each polyset As AnnotationContainer In Me
          polyset.ToXML(SVGXMLFile)
        Next
        SVGXMLFile.WriteEndElement()
        SVGXMLFile.WriteEndDocument()
        SVGXMLFile.Close()

        If Not oUri.IsFile Then
          'Trigger upload by requesting response
          request.GetResponse()
        End If
      Else
        'Just erase the file if it exists
        If oUri.IsFile Then
          Dim strFile As String = oUri.LocalPath
          If System.IO.File.Exists(strFile) Then
            System.IO.File.Delete(strFile)
          End If
        Else
          'We should create an empty stream and make sure the httphandler erase the file ...
          Dim oWebclient As New System.Net.WebClient

          'Normally method is PUT, but when using encryption we use a special httphandler requiring POST
          Dim strMethod As String = "PUT"
          If oUri.Query.Length > 0 Then strMethod = "POST"

          Dim oStream As IO.Stream = oWebclient.OpenWrite(oUri.ToString, strMethod)
          oStream.Close()
        End If
      End If
    End Sub

#End Region

#Region "Draw and related Methods"
    Public Sub Draw(ByVal g As Graphics, ByVal zoom As Single, ByVal offset As Point)
      If Me.Visible Then
        'Quality settings
        g.PixelOffsetMode = PixelOffsetMode.HighQuality
        g.TextRenderingHint = Drawing.Text.TextRenderingHint.ClearTypeGridFit
        g.SmoothingMode = SmoothingMode.AntiAlias

        'Create a transform that takes the zoom and offset into account.
        g.Transform.Reset()
        g.TranslateTransform(-zoom * offset.X, -zoom * offset.Y)
        g.ScaleTransform(zoom, zoom)

        'Create the Drawinfo struct we use for drawing the polygons.
        'Note that these dimensions take the scaling in graphics into account!
        _drawingInfo.MinFontSize = CSng(9 / zoom)
        _drawingInfo.MinPenSize = CSng(1 / zoom)
        _drawingInfo.MinPointSize = CSng(2 / zoom)
        If Me._dimensionMode = LineAndTextDimensionMode.Image Then
          _drawingInfo.Unit = 1
        Else
          _drawingInfo.Unit = CSng(1.0 / zoom)
        End If

        For Each container As AnnotationContainer In Me
          container.Draw(g, _drawingInfo)
        Next
        g.Transform.Reset()
      End If
    End Sub

    ''' <summary>
    ''' Render all annotations into a bitmap.
    ''' </summary>
    ''' <param name="image"></param>
    ''' <remarks></remarks>
    Public Sub RenderIntoBitmap(ByVal image As Bitmap)
      RenderIntoBitmap(image, 1)
    End Sub

    ''' <summary>
    ''' Render all annotations into a bitmap at given zoom factor.
    ''' </summary>
    ''' <param name="image"></param>
    ''' <param name="zoom"></param>
    ''' <remarks></remarks>
    Public Sub RenderIntoBitmap(ByVal image As Bitmap, ByVal zoom As Single)
      Dim g As Graphics = Graphics.FromImage(image)
      Me.Draw(g, zoom, New Point(0, 0))
      g.Dispose()
    End Sub

    ''' <summary>
    ''' Check wether a point is inside an annotation
    ''' </summary>
    ''' <param name="ann"></param>
    ''' <param name="pt"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function IsPointVisible(ByVal ann As Annotation, ByVal pt As PointF) As Integer
      Return ann.IsPointVisible(pt, Me._drawingInfo)
    End Function


    ''' <summary>
    ''' Check whether a point lies on the annotation itself
    ''' </summary>
    ''' <param name="ann"></param>
    ''' <param name="pt"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function IsOutlineVisible(ByVal ann As Annotation, ByVal pt As PointF) As Boolean
      Return ann.IsOutlineVisible(pt, Me._drawingInfo)
    End Function

    ''' <summary>
    ''' Find the annotation object which has an border at the given coordinates. 
    ''' Return nothing if none is found. Annotation must be visible and enabled!
    ''' </summary>
    ''' <param name="pt">The point screen coordinates</param>
    ''' <returns>The object or nothing</returns>
    ''' <remarks>
    ''' </remarks>
    Public Function GetAnnotation(ByVal pt As PointF) As Annotation
      Return GetAnnotation(pt, True, True)
    End Function

    ''' <summary>
    ''' Find the annotation object which has a border at the given coordinates. 
    ''' Return nothing if none is found. 
    ''' </summary>
    ''' <param name="pt">The point image coordinates</param>
    ''' <param name="mustBeVisible">Whether the annotation must be visible to be returned</param>
    ''' <returns>The object or nothing</returns>
    ''' <remarks>Only enabled annotations can be returned
    ''' </remarks>
    Public Function GetAnnotation(ByVal pt As PointF, ByVal mustBeVisible As Boolean, ByVal mustBeEnabled As Boolean) As Annotation
      If ((mustBeVisible And Me.Visible) Or Not mustBeVisible) And ((mustBeEnabled And Me.Enabled) Or Not mustBeEnabled) Then
        For Each container As AnnotationContainer In Me
          If ((mustBeVisible And container.Visible) Or Not mustBeVisible) And ((mustBeEnabled And container.Enabled) Or Not mustBeEnabled) Then
            For Each ann As Annotation In container
              If ((mustBeVisible And ann.Visible) Or Not mustBeVisible) And ((mustBeEnabled And ann.Enabled) Or Not mustBeEnabled) Then
                If ann.IsOutlineVisible(pt, Me._drawingInfo) Then
                  Return ann
                End If
              End If
            Next
          End If
        Next
      End If
      Return Nothing
    End Function

#End Region
  End Class
End Namespace
